Zajmiemy się teraz widgetem wyboru ilości produktu, jednak tym razem nie będzie to kolejna metoda klasy Product, a nowa klasa! Dlaczego?
Cała idea OOP opiera się na tym, aby oddzielać od siebie grupy funkcjonalności. O ile metoda processOrder czy renderInMenu możemy jak najbardziej uznać za takie, które tyczą się konkretnie produktu, to widget ilości już nie. Owszem, będziemy z niego korzystać w klasie Product, ale czy tylko tam? Widget ilości to dość uniwersalna funkcjonalność. Na pewno przyda się również w innych miejscach, chociażby w koszyku. Przecież często widzimy w e-sklepach mechanizm, w którym po dodaniu produktu do koszyka, nadal można zmienić ilość sztuk. A kto wie, może pojawią się jeszcze kolejne miejsca, w których taki widget się przyda?
Gdybyśmy zamknęli go w klasie Product, to przy próbie użycia go jeszcze raz, gdzie indziej, zwyczajnie musielibyśmy powielić jego logikę. Jeśli wydzielimy go jednak do osobnej klasy, to bez problemu skorzystamy z jej instancji w produktach, ale i np. koszyku.
Nowa klasa będzie trochę inna niż Product. Nasza pierwsza klasa otrzymywała w konstruktorze informacje o nazwie i strukturze produktu, ale resztę robiła już sama, np. renderowanie reprezentacji produktu w HTML było już jej rolą. Podobnie jak zdynamizowanie formularza, aby zmiany opcji faktycznie przeliczały cenę. W naszej nowej klasie (nazwiemy ją AmountWidget) będzie trochę inaczej.
Rolą AmountWidget ma być po prostu nadanie życia inputom liczbowym. Tak, aby można było łatwo i przyjemnie, za pomocą buttonów "+" i "-", zwiększać lub zmniejszać wartość pola. Oczywiście znajdzie się tam również walidacja, tak, aby nie można było wybrać liczby za małej albo za dużej lub wpisać tekstu. Czy jednak będzie zajmować się również tworzeniem samego elementu? Nie. Zwróć uwagę, że przecież input ilości sztuk już na stronie jest. Został wygenerowany przez instancje klasy Product. Nie trzeba go tworzyć od zera.
To duża zmiana, oznacza to bowiem, że AmountWidget nie będzie niczego sam generował. Zamiast tego otrzyma tylko referencję do odpowiedniego elementu jako argument konstruktora, a potem odpowiednio się nim zajmie.
Całość ma działać następująco:
Przy okazji zauważ jeszcze jedną funkcjonalność naszego widgetu. Ewidentnie widzimy, że cena produktu przelicza się każdorazowo przy zmianie ilości sztuk. Oznacza to, że na pewno nasz widget powinien być w stanie informować inne elementy np. instancje Product o zmianie takiej wartości. Tak, aby te miały możliwość zareagowania na to np. ponownym uruchomieniem metody processOrder.
No dobrze, trochę już wiemy, ale trzeba przejść do działania. Od czego zaczniemy?
Wiemy już, że instancje tej klasy nie będą musiały tworzyć własnych elementów DOM, ponieważ zostały one już stworzone przez instancje produktów. W takim razie konstruktor musi tylko otrzymywać odniesienie (referencję) do elementu, w którym widget ma zostać zainicjowany, żeby wiedzieć "na czym" pracować.
Co ważne tym elementem nie będzie sam input, lecz div, w którym takowy input się znajduje. Dlaczego? Bo wraz z inputem potrzebujemy mieć jeszcze dostęp do buttonów "+" i "-". W końcu będą ona dla nas istotne. Zamiast inputu będziemy więc przekazywać cały div:
Właśnie na tego typu element konstruktor klasy AmountWidget będzie oczekiwać. Dlatego też na pewno zaczniemy od przygotowania odpowiedniego konstruktora oraz funkcji, która stworzy właściwości z referencjami do trzech elementów otrzymanych w tym divie.
- inputu z wartością,
- linku zmniejszający wartość,
- linku zwiększający wartość.
Po co nam te referencje? Znasz już ten koncept. Żeby raz, w jednym miejscu, przygotować sobie "skrócony" dostęp do elementów, a potem w innych metodach łatwiej z nich korzystać. Podobnie postąpiliśmy w klasie Product, dodając metodę getElements. Przygotowaliśmy tam referencję, którą następnie wykorzystywaliśmy w innych metodach.
Do naszej klasy na pewno dodamy również metodę informowania instancji produktu o tym, że wartość została zmieniona. Dzięki temu w momencie zmiany zamawianej ilości sztuk, cena produktu będzie mogła się natychmiast przeliczyć na nowo. Nie wchodźmy na razie w szczegóły, jak to zrobić.
Na końcu zajmiemy się dodaniem limitów, dzięki którym wybór ilości sztuk będzie ograniczony do zakresu od 1 do 9.
Zacznijmy od stworzenia nowej klasy o nazwie AmountWidget, która na początku będzie zawierać wyłącznie konstruktor. Dodaj deklarację tej klasy przed obiektem app.
Tak jak mówiliśmy, będzie on oczekiwać na jeden element, referencję do diva z inputem i buttonami.
Zwijanie kodu i komentowanie
Na tym etapie może Ci już być ciężko z zapamiętaniem, co i gdzie znajduje się w pliku script.js. Niedługo nauczymy się wydzielać klasy do osobnych plików, ale na razie musimy się jeszcze pomęczyć... Dlatego też warto w tym momencie skorzystać z prostszego rozwiązania, zwijania kodu, dostępnego w większości współczesnych edytorów kodu. Zwykle przy najechaniu kursorem na numer linii zobaczysz ikony pozwalające na zwinięcie np. całej funkcji czy metody.
W ten sposób możesz zwinąć te metody, którymi w danej chwili się nie zajmujesz, aby uzyskać dużo większą przejrzystość kodu.
Drugą kwestią, która może na tym etapie sprawiać więcej problemów niż dawać korzyści, są użyte przez nas console.log. Jeśli jest ich zbyt wiele, w konsoli będzie sporo komunikatów, pośród których trudno będzie się odnaleźć. Najlepiej będzie w tym momencie znaleźć wszystkie wystąpienia console.log i "zakomentować je", czyli dodać // na początku linii, w której występują. Albo... jeśli zachęciliśmy Cię wcześniej do używania debuggera, w ogóle z nich zrezygnować.
W porządku. Nasza klasa już istnieje (choć w bardzo ubogiej formie). Aby jednak sprawdzić, czy jakkolwiek działa, trzeba ją wykorzystać. Jak? Tworząc instancję. Wiemy że, koniec końców, i tak instancje klasy Product będą chciały korzystać z AmountWidget, licząc na to, że klasa ta "ożywi" ich inputy. Możemy wiec równie dobrze zabrać się za stworzenie takiej współpracy już teraz. Przynajmniej upewnimy się od razu, czy konstruktor naszej nowej klasy poprawnie się włącza.
Wróć więc do klasy Product i odnajdź metodę getElements. Mamy tu już kilka referencji, m.in. do formularza czy diva z ceną. Teraz dodamy kolejną do diva z inputem i buttonami "+" i "-". Po co? Bo jak już wspomnieliśmy, AmountWidget będzie potrzebować dostępu do tego elementu.
Dodaj więc nową właściwość thisProduct.amountWidgetElem. Zadbaj o to, aby jej wartością była referencja do elementu o selektorze select.menuProduct.amountWidget. Pamiętaj przy tym, żeby szukać go w divie pojedynczego produktu, a nie całym dokumencie. Inaczej bowiem moglibyśmy "przypadkiem" znaleźć div z inputem z innego produktu (w końcu każdy div produktu ma identyczną strukturę HTML), a tego nie chcemy.
Następnie, wciąż w klasie Product, dodaj nową metodę initAmountWidget. Będzie ona odpowiedzialna za utworzenie nowej instancji klasy AmountWidget i zapisywanie jej we właściwości produktu. Po to, aby w razie potrzeby mieć do niej łatwy dostęp.
Zauważ, że od razu przekazujemy do konstruktora referencję do naszego diva z inputem i buttonami tak, jak oczekiwała na to klasa AmountWidget. Przy czym powtórzmy jeszcze raz – przekazujemy tylko referencję, tylko adres. Duże obiekty zawsze są przekazywane tylko jako referencja, pamiętasz?
Na koniec, w konstruktorze klasy Product wywołaj tę metodę, tuż przed wywołaniem metody processOrder.
W rezultacie, w konsoli powinny pojawić się komunikaty z console.log użytych w konstruktorze klasy AmountWidget. Pojawią się osobno dla każdego produktu.
Widzimy w tych komunikatach, że żadna z instancji AmountWidget nie ma jeszcze właściwości i każda wyświetla taki sam element. Nie jest to jednak ten sam jeden element, tylko podobny z każdego produktu. Możesz to łatwo sprawdzić, klikając prawym przyciskiem myszy na jednym z elementów wyświetlonych w konsoli i wybierając opcję "Reveal in Elements panel".
Czas na powrót do klasy AmountWidget. Już wcześniej wspominaliśmy, że będziemy w niej korzystać z trzech elementów – inputu i dwóch buttonów. Warto więc przygotować do nich referencje. Dla czytelności, podobnie jak w klasie Product, zrobimy w nowej dedykowanej metodzie – getElements.
Stworzymy ją teraz. Różnica jest jedna, tym razem będziemy przekazywać tej metodzie argument element otrzymany przez konstruktor. Dlaczego?
W przypadku klasy Product, zanim uruchomimy funkcję getElements, jest wywoływana metoda renderInMenu, która m.in. szykuje nam właściwość thisProduct.element. Dlatego też w getElements możemy od razu z niej korzystać. W końcu, jeśli zapiszemy coś do właściwości instancji, to możemy z tego korzystać w każdej jej metodzie.
W AmountWidget sytuacja jest inna. Nie mamy funkcji renderInMenu, nie szykowaliśmy też wcześniej właściwości thisWidget.element. Tak naprawdę jedynym miejscem, gdzie mamy dostęp do diva otrzymanego w instancji jest argument konstruktora. A czy argument funkcji constructor będzie dostępny ot tak w metodzie getElements? Nie. W końcu argument funkcji jest dostępny tylko w jej zakresie. Tym samym najprościej możemy po prostu przekazać zawartość tego argumentu konstruktora dalej... jako argument kolejnej metody getElements.
Dodaj więc do konstruktora następujące wywołanie:
thisWidget.getElements(element);
A następnie nową metodę:
getElements(element){
const thisWidget = this;
thisWidget.element = element;
thisWidget.input = thisWidget.element.querySelector(select.widgets.amount.input);
thisWidget.linkDecrease = thisWidget.element.querySelector(select.widgets.amount.linkDecrease);
thisWidget.linkIncrease = thisWidget.element.querySelector(select.widgets.amount.linkIncrease);
}
Swoją drogą, zauważ jak ważna jest wiedza o referencjach. Argument element, który otrzymaliśmy w konstruktorze jest tylko referencją do tego samego elementu DOM, co thisProduct.amountWidgetElem. Kiedy przekazujemy argument element niżej, do getElements, to dalej przekazujemy tę samą referencję. Czyli tak naprawdę argument element w getElements to wciąż referencja do jednego i tego samego obiektu. Tego samego elementu DOM, na który wskazywał również thisProduct.amountWidgetElem. Idea referencji jest niesamowita, prawda? Gdyby nie ona, mielibyśmy już w tym momencie trzy kopie tego samego obiektu, a tak wciąż działamy na jednym i tym samym. To znacznie wydajniejsze.
Po tym kroku powinny zmienić się komunikaty w konsoli – teraz instancje klasy AmountWidget nie są już puste, tylko mają właściwości, w których zapisaliśmy elementy widgetu.
Użytkownik strony domyślnie może wpisać w inpucie co chce. Przydałoby się, żeby nasz widget na to nie pozwalał. Potrzebujemy jakiejś funkcji pośrednika, która uruchamiałaby się w momencie zmiany wartości, kontrolowała co jest wpisane i dopiero potem decydowała, czy zostawić taką nową wartość, czy może jednak nie. Musi więc również pamiętać jaka wartość była wpisana wcześniej. Wtedy w razie wpisania czegoś błędnego, będzie w stanie przywrócić wcześniejszą poprawną wartość.
Zanim jednak zabierzemy się za taką funkcję na poważnie, dla przypomnienia spójrz na gif, który pokazuje, jakie są nasze oczekiwania:
Próba wpisania zbyt dużej wartości (12) lub tekstu (abc) kończy się przywróceniem starej. Zatem na pewno, tak jak mówiliśmy, będziemy musieli stworzyć funkcję, która będzie uruchamiana przy próbie zmiany wartości i decydować, czy ma na to pozwolić, czy może przywrócić starą (ostatnią dobrą) wartość.
Naszą funkcją pośrednikiem będzie nowa metoda – setValue. Dodaj ją teraz do naszej klasy.
To oczywiście dopiero początek. Na razie ta metoda tylko zapisuje we właściwości thisWidget.value wartość przekazanego argumentu, po przekonwertowaniu go na liczbę, a następnie aktualizuje wartość samego inputu. Na razie bez żadnej walidacji nawet nie sprawdzając, czy nie wpisaliśmy czasem czegoś złego... Jednak spokojnie, zaraz się tym zajmiemy. A po co ta konwersja (parseInt)? Pamiętaj, że każdy input, nawet o typie number, zawsze zwraca wartość w formacie tekstowym. Nawet wpisanie więc 10 da nam nie liczbę 10, a tekst '10'. parseInt zadba o konwersję takiej przykładowej '10' do liczby 10.
Tak jak wspomnieliśmy, na razie wpisujemy do thisWidget.value wprost to, co ta metoda otrzyma, ale zaraz to zmienimy. Chcemy jeszcze dodatkowo sprawdzać, czy wartość tej stałej jest poprawna i mieści się w dopuszczalnym zakresie – tylko w takim przypadku zostanie ona zapisana jako właściwość thisWidget.value.
Zaczniemy od najprostszego ifa. Sprawdzimy, czy wartość, która przychodzi do funkcji, jest inna niż ta, która jest już aktualnie w thisWidget.value. Powinien on warunkować, czy linijka thisWidget.value = newValue ma się w ogóle wykonać.
Spróbuj napisać takiego ifa bez naszej pomocy.
Pokaż odpowiedź
Ukryj odpowiedź
if(thisWidget.value !== newValue) {
thisWidget.value = newValue;
}
To już jakaś zmiana. Teraz thisWidget.value zmieni się już faktycznie tylko wtedy, jeśli nowa wpisana w input wartość będzie inna niż obecna.
W takim razie czas na kolejne ćwiczenie. Chcielibyśmy również, żeby nasza funkcja ustalała, czy to wpisano w input jest faktycznie liczbą. Jak możemy to sprawdzić?
Zwróć uwagę, że na początku naszej funkcji staramy się konwertować podane value do liczby. Jeśli parseInt natrafi na tekst, którego nie da się skonwertować na liczbę (np. abc), to najprościej w świecie zwróci null. Wystarczy więc sprawdzić w naszym warunku, czy oprócz tego, że thisWidget.value !== newValue, newValue nie jest też null-em. No bo tylko jeśli nie jest, możemy mieć pewność, że to liczba i faktycznie zaktualizować wartość thisWidget.value.
Spróbuj dopisać taki warunek bez naszej pomocy. Przyda Ci się znajomość funkcji isNaN. Zajrzyj więc do dokumentacji.
Pokaż odpowiedź
Ukryj odpowiedź
if(thisWidget.value !== newValue && !isNaN(newValue)) {
thisWidget.value = newValue;
}
To już coś. Wyobraź sobie, jak w tej chwili mogłaby działać nasza funkcja. Powiedzmy, że za pierwszym razem ktoś wpisał w input '3'. Nasza funkcja setValue otrzymałaby taką wartość tekstową, przekonwertowała ją do liczby ('3' -> 3), a następnie sprawdziła warunek. Na samym początku właściwość thisWidget.value nawet nie istnieje, więc na pewno jest inne niż newValue (3). undefined w końcu nie jest równe 3. Czy 3 do tego nie jest NaN-em? No nie jest, więc warunek jest w pełni spełniony! Skoro tak, to od tej chwili thisWidget.value równa się 3 i taką wartość za chwilę dopisujemy też do samego inputu. Co prawda nie było to konieczne, bo użytkownik przecież sam wpisał taką wartość w input, więc i tak już ona tam jest, ale... czy to coś zepsuje? Nie.
Powiedzmy, że za chwilę wpisano kolejną wartość, tym razem celowo niepoprawną, np. abc. Oczywiście znowu uruchomi się nasza funkcja i spróbuje skonwertować wartość do liczby, tym razem jednak zakończy się to porażką. Otrzymamy jako newValue wartość null. W takim razie nasz warunek da nam false i nie wykona instrukcji z ifa. Nie zmieni więc wartości thisWidget.value. Ta wciąż będzie równa 3. Tym samym, kiedy wykona się ostatnia linijka funkcji, a więc przypisanie wartości thisWidget.value do inputu, to tekst abc, który wpisał użytkownik, zostanie nadpisany wartością 3! Dokładnie tak, jak na gifie!
Przyznaj, ciekawe rozwiązanie.
No dobrze, tylko że brakuje nam jeszcze jednego puzzla w układance. Napisaliśmy przed chwilą, że zmiana w inpucie ma włączyć funkcję setValue, ale czy właściwie to robi? Nie. Przecież JS sam z siebie nie domyśli się, o co nam chodzi. Musimy skorzystać z odpowiednich nasłuchiwaczy. Zaraz je dodamy.
Zanim jednak się tym zajmiemy, zrób jeszcze jedną rzecz. Wywołaj tę metodę w konstruktorze, pod wywołaniem metody getElements. Chodzi o to, żeby nawet na samym starcie, kiedy nikt jeszcze nie zmienił wartości w inpucie, nasza instancja miała już informację co w tym inpucie jest. Bo tak naprawdę tam zawsze coś jest. Gdy produkt generuje swój HTML, to w inpucie od razu wstawia nam domyślną wartość. Dobrze, żeby nasz widget o tym wiedział.
Zmiana w tym kroku będzie stosunkowo mała – w komunikatach w konsoli możesz zobaczyć, że teraz każdy widget dodatkowo ma właściwość value równą 1.
Czas na nasze nasłuchiwacze. Dodaj kolejną metodę, tym razem nazwij ją initActions. W tej klasie dodamy trzy listenery eventów:
- dla
thisWidget.input dodaj listener eventu change, dla którego handler użyje metody setValue z takim samym argumentem, jak w konstruktorze (czyli z wartością inputa),
- dla
thisWidget.linkDecrease dodaj listener eventu click, dla którego handler powstrzyma domyślną akcję dla tego eventu, oraz użyje metody setValue – tym razem argumentem będzie thisWidget.value pomniejszone o 1,
thisWidget.linkIncrease potraktuj tak samo, jak thisWidget.linkDecrease, z tym że argumentem będzie thisWidget.value powiększone o 1.
Następnie zadbaj o to, aby ta metoda uruchamiała się automatycznie, od razu po utworzeniu instancji.
Po wykonaniu ćwiczenia sprawdź jak input ilości sztuk w produkcie reaguje na wpisanie tekstu.
Powinien przywracać poprzednią wartość, tak jak na gifie wyżej.
W tej chwili możemy zmniejszać ilość sztuk nawet poniżej zera albo wpisywać bardzo duże wartości, nawet w tysiącach. Chcielibyśmy, aby wartości były sprawdzane według jakiegoś zakresu. Nie większe niż maksymalna wspierana wartość i mniejsza niż minimalna. Wymaga to dodania kolejnych dwóch warunków do ifa w metodzie setValue.
Potraktuj to jako kolejne ćwiczenie. Na pewno dasz sobie radę! Minimalna wartość jest dostępna w settings.amountWidget.defaultMin, a maksymalna w settings.amountWidget.defaultMax.
Po zmianach input powinien działać znacznie lepiej:
Zauważ, że teraz możemy poruszać się już tylko w zakresie 0-10. Właśnie o to nam chodziło!
Jak wspomnieliśmy na początku tego submodułu, nasz widget musi jeszcze w jakiś sposób informować produkt, że zmieniła się liczba sztuk. Tak, aby ten mógł ponownie przeliczyć całkowitą cenę. Moglibyśmy skorzystać z wbudowanego eventu change uruchamianego na inpucie po zmianie jego wartości przez użytkownika strony, ale:
- jeśli zmieniamy wartość za pomocą JS po kliknięciu w guzik ("+" albo "-"), ten event nie będzie się uruchamiał automatycznie, musielibyśmy go uruchomić ręcznie,
- jeśli użytkownik wpisze niepoprawną wartość, zostanie uruchomiony event
change jeszcze zanim nasz skrypt sprawdzi, czy ta wartość jest poprawna, więc produkt od razu próbowałby przeliczyć cenę... nawet dla wartości niepoprawnej, takiej jak test, a to nie ma prawa się udać.
Dlatego zrobimy coś innego – wywołamy własny, customowy event!
Do tej pory tylko nasłuchiwaliśmy, czy jakiś event się wydarzył – np. czy link został kliknięty. W tym wypadku to akcja użytkownika strony (kliknięcie w link) wywoływała event.
Tym razem sami wywołamy event! Tak, możemy to zrobić. Sami wybierzemy nawet jego nazwę. Dzięki temu produkt będzie mógł nasłuchiwać nie na event change, ale np. update i kiedy go wychwyci, będzie wiedział, że należy zaktualizować cenę produktu, a wartość w inpucie jest na pewno poprawna. Bo na pewno została już sprawdzona.
Wywołanie eventu
Zacznijmy od stworzenia metody announce. Będzie ona tworzyła instancje klasy Event, wbudowanej w silnik JS (czyli w przeglądarkę). Jest to klasa odpowiedzialna właśnie za stworzenie obiektu "eventu". Następnie, ten event zostanie wyemitowany na kontenerze naszego widgetu.
Możesz sobie wyobrazić, że jeśli użytkownik klika gdzieś na stronie, to przeglądarka robi dokładnie to samo co my teraz. Również tworzy event click w podobny sposób przy użyciu klasy Event, a następnie emituje go na tym klikniętym elemencie za pomocą metody dispatchEvent. Ma to sens?
Nasz event nazwaliśmy 'updated', ale to zupełnie zmyślona nazwa – równie dobrze moglibyśmy użyć każdego innego określenia, które nie jest jednym z wbudowanych eventów.
Wywoływanie wbudowanych eventów
Wbudowane eventy również można wywoływać – np. możemy wywołać event click na linku. Nie wywoła to domyślnej akcji (przejścia na adres podany w atrybucie href tego linka), ale zostanie wychwycone przez każdy event listener dodany w JS dla tego linka.
Na przykład, jeśli wywołamy event click na guziku zwiększania ilości, to zadziała on tak samo, jak gdybyśmy go kliknęli. Krótko mówiąc, jesteśmy w stanie sami wchodzić w body "przeglądarki" i symulować nawet wbudowane już w nią eventy.
Teraz musimy jeszcze wywoływać tę metodę announce. Gdzie? Koniecznie musimy zadbać o to, aby uruchamiała się dopiero wtedy, kiedy nowa wartość, którą chcemy ustawić, faktycznie jest poprawna. Tylko wtedy jest sens informować o zmianie produkt. Właśnie tym nasz event updated będzie się różnił od eventu change, że nasz uruchomi się przy zmianie wartości, ale tylko na taką, która wciąż będzie poprawna.
Zastanów się więc, w którym miejscu w metodzie setValue musimy ją wstawić.
Nasłuchiwanie eventu
Drugą częścią informowania produktu, jak już wspomnieliśmy, jest nasłuchiwanie tego eventu w klasie Product. Co bowiem z tego, że event updated będzie emitowany na inpucie, skoro produkt nic sobie z tym nie robi?
Przejdź więc do klasy Product i znajdź w niej metodę initAmountWidget. Następnie dodaj do niej listener eventu, który będzie nasłuchiwał na element thisProduct.amountWidgetElem, na zdarzenie updated. Dlaczego nasłuchujemy właśnie na ten element? Bo to na nim emitowaliśmy nasz event. Pamiętaj, w końcu thisWidget.element to referencja do tego samego identycznego elementu co thisProduct.amountWidgetElem.
Jako funkcja, która ma uruchomić się w momencie wykrycia tego eventu, dodaj prostą funkcję anonimową, która zajmie się uruchamianiem metody thisProduct.processOrder();.
Ten kod już powinien działać, ale nie będzie widać żadnych jego efektów. Nic dziwnego, w końcu metoda processOrder w żaden sposób nie sprawdza wybranej liczby sztuk, ani tym bardziej nie mnoży przez nią ceny końcowej.
Dlatego musimy zrobić jeszcze jedną zmianę. Znajdź metodę processOrder. Na jej końcu powinna być linia kodu, która ustawia zawartość thisProduct.priceElem na wartość zmiennej price. Tuż przed tą linią dodaj ten fragment kodu:
W ten sposób, tuż przed wyświetleniem ceny obliczonej z uwzględnieniem opcji, pomnożymy ją przez ilość sztuk wybraną w widgecie!
Teraz już cena produktu powinna się zmieniać w momencie zmiany ilości. Jeśli klikniesz w guzik zwiększenia lub zmniejszenia ilości, cena zmieni się natychmiast. Jeśli wpiszesz liczbę w inpucie, cena zmieni się, kiedy wyjdziesz z inputa (np. klikniesz gdzieś na stronie lub wciśniesz klawisz Tab na klawiaturze).
Uff... To już koniec. Dobra robota!